/*
 * Acess OS
 * Ext2 Driver Version 1
 */
/**
 * \file fs/ext2.c
 * \brief Second Extended Filesystem Driver
 * \todo Implement file read support
 */
#include <acess.h>
#include <vfs.h>
#include <fs_ext2_int.h>

//STRUCTURES
typedef struct {
	 int	fd;
	 int	CacheID;
	vfs_node	RootNode;
	
	tExt2_SuperBlock	SuperBlock;
	 int	BlockSize;
	 
	 int	GroupCount;
	tExt2_Group		Groups[];
} tExt2_Disk;

// === PROTOTYPES ===
//Interface Functions
vfs_node	*Ext2_InitDevice(char *device, char **Options);
 int		Ext2_Unmount(vfs_node *Node, char *NewName);
 int		Ext2_Read(vfs_node *node, Uint64 offset, Uint64 length, void *buffer);
 int		Ext2_Write(vfs_node *node, Uint64 offset, Uint64 length, void *buffer);
vfs_node	*Ext2_ReadDir(vfs_node *node, int dirPos);
vfs_node	*Ext2_FindDir(vfs_node *node, char *filename);
vfs_node	*Ext2_MkNod(vfs_node *node, char *name, int flags);
 int		Ext2_int_GetInode(vfs_node *Node, tExt2_Inode *Inode);
vfs_node	*Ext2_int_CreateNode(tExt2_Disk *Disk, Uint InodeId, char *Name, Uint64 VfsInode);
 int		Ext2_int_ReadInode(tExt2_Disk *Disk, Uint InodeId, tExt2_Inode *Inode);
Uint64		Ext2_int_GetBlockAddr(tExt2_Disk *Disk, Uint32 *Blocks, int BlockNum);

// === SEMI-GLOBALS ===
tExt2_Disk	gExt2_disks[6];
 int	giExt2_count = 0;
Uint16	gaExt2_MBRTypes[] = {0x83,0};
tVFS_Driver	gExt2_FSInfo = {NULL,
	"ext2",
	Ext2_InitDevice,
	NULL, gaExt2_MBRTypes, NULL
	};

// === CODE ===

/**
 * \fn void Ext2_Install()
 * \brief Install the Ext2 Filesystem Driver
 */
void Ext2_Install()
{
	//vfs_addfs(0x83, Ext2_InitDevice);
	VFS_AddDriver( &gExt2_FSInfo );
}

/**
 \fn vfs_node *Ext2_initDevice(char *Device, char **Options)
 \brief Initializes a device to be read by by the driver
 \param Device	String - Device to read from
 \param Options	NULL Terminated array of option strings
 \return Root Node
*/
vfs_node *Ext2_InitDevice(char *Device, char **Options)
{
	tExt2_Disk	*disk;
	 int	fd;
	 int	groupCount;
	tExt2_SuperBlock	sb;
	tExt2_Inode	inode;
	
	// Open Disk
	fd = vfs_open(Device, VFS_OPENFLAG_READ|VFS_OPENFLAG_WRITE);		//Open Device
	if(fd == -1) {
		WarningEx("EXT2", "Unable to open '%s'\n", Device);
		return NULL;
	}
	
	// Read Superblock at offset 1024
	VFS_ReadAt(fd, 1024, 1024, &sb);	// Read Superblock
	
	// Sanity Check Magic value
	if(sb.s_magic != 0xEF53) {
		WarningEx("EXT2", "Volume '%s' is not an EXT2 volume\n", Device);
		vfs_close(fd);
	}
	
	// Get Group count
	groupCount = DivUp(sb.s_blocks_count, sb.s_blocks_per_group);
	//LogF(" Ext2_initDevice: groupCount = %i\n", groupCount);
	
	// Allocate Disk Information
	disk = malloc(sizeof(tExt2_Disk) + sizeof(tExt2_Group)*groupCount);
	disk->fd = fd;
	memcpyd(&disk->SuperBlock, &sb, 1024/4);
	disk->GroupCount = groupCount;
	
	// Get an inode cache handle
	disk->CacheID = inode_initCache();
	
	// Get Block Size
	//LogF(" Ext2_initDevice: s_log_block_size = 0x%x\n", sb.s_log_block_size);
	disk->BlockSize = 1024 << sb.s_log_block_size;
	
	// Read Group Information
	VFS_ReadAt(disk->fd,
		sb.s_first_data_block * disk->BlockSize + 1024,
		sizeof(tExt2_Group)*groupCount,
		disk->Groups);
	
	#if 0
	LogF(" Ext2_initDevice: Block Group 0\n");
	LogF(" Ext2_initDevice: .bg_block_bitmap = 0x%x\n", disk->Groups[0].bg_block_bitmap);
	LogF(" Ext2_initDevice: .bg_inode_bitmap = 0x%x\n", disk->Groups[0].bg_inode_bitmap);
	LogF(" Ext2_initDevice: .bg_inode_table = 0x%x\n", disk->Groups[0].bg_inode_table);
	LogF(" Ext2_initDevice: Block Group 1\n");
	LogF(" Ext2_initDevice: .bg_block_bitmap = 0x%x\n", disk->Groups[1].bg_block_bitmap);
	LogF(" Ext2_initDevice: .bg_inode_bitmap = 0x%x\n", disk->Groups[1].bg_inode_bitmap);
	LogF(" Ext2_initDevice: .bg_inode_table = 0x%x\n", disk->Groups[1].bg_inode_table);
	#endif
	
	// Get root Inode
	Ext2_int_ReadInode(disk, 2, &inode);
	
	// Create Root Node
	memsetd(&disk->RootNode, 0, sizeof(vfs_node)/4);
	disk->RootNode.name = "ext2";	disk->RootNode.nameLength = 4;
	disk->RootNode.inode = 0;	// Root node ID (not actually it's inode, to allow for hard links)
	disk->RootNode.impl = 2;	// Root Inode
	disk->RootNode.ImplPtr = disk;	// Save disk pointer
	disk->RootNode.length = -1;	// Fill in later (on readdir)
	disk->RootNode.flags = VFS_FFLAG_DIRECTORY;
	
	disk->RootNode.readdir = Ext2_ReadDir;
	disk->RootNode.finddir = Ext2_FindDir;
	disk->RootNode.relink = Ext2_Unmount;
	
	// Complete root node
	disk->RootNode.uid = inode.i_uid;
	disk->RootNode.gid = inode.i_gid;
	disk->RootNode.mode = 0666;	//RW-RW-RW-
	
	#if 0
	LogF(" Ext2_InitDevice: inode.i_size = 0x%x\n", inode.i_size);
	LogF(" Ext2_InitDevice: inode.i_block[0] = 0x%x\n", inode.i_block[0]);
	#endif
	
	return &disk->RootNode;
}

/**
 * \fn int Ext2_Unmount(vfs_node *Node, char *NewName)
 * \brief Close a mounted device
 */
int Ext2_Unmount(vfs_node *Node, char *NewName)
{
	tExt2_Disk	*disk = Node->ImplPtr;
	if(!NewName)
	{
		vfs_close( disk->fd );
		inode_clearCache( disk->CacheID );
		memsetd(disk, 0, sizeof(*disk)+disk->GroupCount*sizeof(tExt2_Group));
		free(disk);
	}
	return 1;
}

/**
 * \fn int Ext2_Read(vfs_node *Node, Uint64 Offset, Uint64 Length, void *Buffer)
 * \brief Read from a file
 */
int Ext2_Read(vfs_node *Node, Uint64 Offset, Uint64 Length, void *Buffer)
{
	tExt2_Disk	*disk = Node->ImplPtr;
	tExt2_Inode	inode;
	Uint64	base;
	Uint	block;
	Uint64	remLen;
	
	// Get Inode
	Ext2_int_GetInode(Node, &inode);
	
	block = Offset / disk->BlockSize;
	Offset = Offset / disk->BlockSize;
	base = Ext2_int_GetBlockAddr(disk, inode.i_block, block);
	
	// Read only block
	if(Length <= disk->BlockSize - Offset)
	{
		VFS_ReadAt( disk->fd, base+Offset, Length, Buffer);
		return Length;
	}
	
	// Read first block
	remLen = Length;
	VFS_ReadAt( disk->fd, base + Offset, disk->BlockSize - Offset, Buffer);
	remLen -= disk->BlockSize - Offset;
	Buffer += disk->BlockSize - Offset;
	block ++;
	
	// Read middle blocks
	while(remLen > disk->BlockSize)
	{
		base = Ext2_int_GetBlockAddr(disk, inode.i_block, block);
		VFS_ReadAt( disk->fd, base, disk->BlockSize, Buffer);
		Buffer += disk->BlockSize;
		remLen -= disk->BlockSize;
		block ++;
	}
	
	// Read last block
	base = Ext2_int_GetBlockAddr(disk, inode.i_block, block);
	VFS_ReadAt( disk->fd, base, remLen, Buffer);
	
	return Length;
}

/**
 * \fn int Ext2_Write(vfs_node *Node, Uint64 Offset, Uint64 Length, void *Buffer)
 * \brief Write to a file
 */
int Ext2_Write(vfs_node *Node, Uint64 Offset, Uint64 Length, void *Buffer)
{
	tExt2_Disk	*disk = Node->ImplPtr;
	tExt2_Inode	inode;
	Uint64	base;
	Uint64	retLen;
	Uint	block;
	Uint64	allocSize;
	 int	bNewBlocks = 0;
	
	Ext2_int_GetInode(Node, &inode);
	
	// Round size up to block size
	// block size is a power of two, so this will work
	allocSize = (inode.i_size + disk->BlockSize) & ~(disk->BlockSize-1);
	if( Offset < allocSize )
	{
		if(Offset + Length > allocSize) {
			bNewBlocks = 1;
			retLen = allocSize - Offset;
		} else
			retLen = Length;
		// Within the allocated space
		block = Offset / disk->BlockSize;
		Offset %= disk->BlockSize;
		base = Ext2_int_GetBlockAddr(disk, inode.i_blocks, block);
		
		// Write only block (if only one)
		if(Offset + retLen <= disk->BlockSize) {
			VFS_WriteAt(disk->fd, base+Offset, retLen, Buffer);
			if(bNewBlocks)	return Length;
			goto addBlocks;	// Ugh! A goto, but it seems unavoidable
		}
		
		// Write First Block
		VFS_WriteAt(disk->fd, base+Offset, disk->BlockSize-Offset, Buffer);
		Buffer += disk->BlockSize-Offset;
		retLen -= disk->BlockSize-Offset;
		block ++;
		
		// Write middle blocks
		while(retLen > disk->BlockSize)
		{
			base = Ext2_int_GetBlockAddr(disk, inode.i_blocks, block);
			VFS_WriteAt(disk->fd, base, disk->BlockSize, Buffer);
			Buffer += disk->BlockSize;
			retLen -= disk->BlockSize;
			block ++;
		}
		
		// Write last block
		base = Ext2_int_GetBlockAddr(disk, inode.i_blocks, block);
		VFS_WriteAt(disk->fd, base, retLen, Buffer);
		if(bNewBlocks)	return Length;	// Writing in only allocated space
	}
	
addBlocks:
	///\todo Implement block allocation
	
	return 0;
}

/**
 * \fn int Ext2_CloseFile(vfs_node *Node)
 * \brief Close a file (Remove it from the cache)
 */
int Ext2_CloseFile(vfs_node *Node)
{
	tExt2_Disk	*disk = Node->ImplPtr;
	inode_uncacheNode(disk->CacheID, Node->impl);
	return 1;
}

/**
 \fn vfs_node *Ext2_ReadDir(vfs_node *Node, int Pos)
 \brief Reads a directory entry
*/
vfs_node *Ext2_ReadDir(vfs_node *Node, int Pos)
{
	tExt2_Inode	inode;
	char	namebuf[EXT2_NAME_LEN+1];
	tExt2_DirEnt	dirent;
	Uint64	Base;	// Block's Base Address
	 int	block = 0, ofs = 0;
	 int	entNum = 0;
	tExt2_Disk	*disk = Node->ImplPtr;
	Uint64	vfsInode = 0;
	vfs_node	*retNode;
	Uint	size;
	
	//LogF("Ext2_ReadDir: (Node=%p, Pos=%i)\n", Node, Pos);
	
	// Check Node Cache
	vfsInode = Node->impl | ((Uint64)Pos << 32);
	if( (retNode = inode_getCache(disk->CacheID, vfsInode)) )
		return retNode;
	
	// Read directory's inode
	Ext2_int_GetInode(Node, &inode);
	size = inode.i_size;
	
	//LogF(" Ext2_ReadDir: inode.i_block[0] = 0x%x\n", inode.i_block[0]);
	
	// Find Entry
	// Get First Block
	// - Do this ourselves as it is a simple operation
	Base = inode.i_block[0] * disk->BlockSize;
	while(Pos -- && size > 0)
	{
		VFS_ReadAt( disk->fd, Base+ofs, sizeof(tExt2_DirEnt), &dirent);
		ofs += dirent.rec_len;
		size -= dirent.rec_len;
		entNum ++;
		
		if(ofs >= disk->BlockSize) {
			block ++;
			if( ofs > disk->BlockSize ) {
				WarningEx("EXT2",
					"Directory Entry %i of inode %i ('%s') extends over a block boundary, ignoring\n",
					entNum-1, Node->impl, Node->name);
			}
			ofs = 0;
			Base = Ext2_int_GetBlockAddr( disk, inode.i_block, block );
		}
	}
	
	if(size <= 0)	return NULL;
	
	// Read Entry
	VFS_ReadAt( disk->fd, Base+ofs, sizeof(tExt2_DirEnt), &dirent );
	//LogF(" Ext2_ReadDir: dirent.inode = %i\n", dirent.inode);
	//LogF(" Ext2_ReadDir: dirent.rec_len = %i\n", dirent.rec_len);
	//LogF(" Ext2_ReadDir: dirent.name_len = %i\n", dirent.name_len);
	VFS_ReadAt( disk->fd, Base+ofs+sizeof(tExt2_DirEnt), dirent.name_len, namebuf );
	namebuf[ dirent.name_len ] = '\0';	// Cap off string
	
	
	// Ignore . and .. (these are done in the vfs / by the shell)
	if(namebuf[0] == '.' && namebuf[1] == '\0')
		return &NULLNode;	// Skip
	if(namebuf[0] == '.' && namebuf[1] == '.' && namebuf[2]=='\0')
		return &NULLNode;	// Same here
	
	//LogF("Ext2_ReadDir: RETURN Node for file '%s'\n", namebuf);
	// Create new node
	return Ext2_int_CreateNode( disk, dirent.inode, namebuf, vfsInode );
}

/**
 \fn vfs_node *Ext2_FindDir(vfs_node *node, char *filename)
 \brief Gets information about a file
 \param node	vfs node - Parent Node
 \param filename	String - Name of file
 \return VFS Node of file
*/
vfs_node *Ext2_FindDir(vfs_node *Node, char *Filename)
{
	tExt2_Disk	*disk = Node->ImplPtr;
	tExt2_Inode	inode;
	char	namebuf[EXT2_NAME_LEN+1];
	tExt2_DirEnt	dirent;
	Uint64	Base;	// Block's Base Address
	 int	block = 0, ofs = 0;
	 int	entNum = 0;
	Uint	size;
	
	// Read directory's inode
	Ext2_int_GetInode(Node, &inode);
	size = inode.i_size;
	
	// Get First Block
	// - Do this ourselves as it is a simple operation
	Base = inode.i_block[0] * disk->BlockSize;
	// Find File
	while(size > 0)
	{
		VFS_ReadAt( disk->fd, Base+ofs, sizeof(tExt2_DirEnt), &dirent);
		VFS_ReadAt( disk->fd, Base+ofs+sizeof(tExt2_DirEnt), dirent.name_len, namebuf );
		namebuf[ dirent.name_len ] = '\0';	// Cap off string
		if(strcmp(namebuf, Filename) == 0)
			return Ext2_int_CreateNode( disk, dirent.inode, namebuf, Node->impl|((Uint64)entNum) );
		ofs += dirent.rec_len;
		size -= dirent.rec_len;
		entNum ++;
		
		if(ofs >= disk->BlockSize) {
			block ++;
			if( ofs > disk->BlockSize ) {
				WarningEx("EXT2",
					"Directory Entry %i of inode %i ('%s') extends over a block boundary, ignoring\n",
					entNum-1, Node->impl, Node->name);
			}
			ofs = 0;
			Base = Ext2_int_GetBlockAddr( disk, inode.i_block, block );
		}
	}
	
	return NULL;
}

/**
 * \fn vfs_node *Ext2_MkNod(vfs_node *Parent, char *Name, int Flags)
 * \brief Create a new node
 */
vfs_node *Ext2_MkNod(vfs_node *Parent, char *Name, int Flags)
{
	return 0;
}

//==================================
//=       INTERNAL FUNCTIONS       =
//==================================


/**
 \internal
 \fn int Ext2_int_GetInode(vfs_node *Node, tExt2_Inode *Inode)
 \brief Gets the inode descriptor for a node
 \param node	node to get the Inode of
 \param inode	Destination
*/
int Ext2_int_GetInode(vfs_node *Node, tExt2_Inode *Inode)
{
	return Ext2_int_ReadInode(Node->ImplPtr, Node->impl, Inode);
}

/**
 * \fn vfs_node *Ext2_int_CreateNode(tExt2_Disk *Disk, Uint InodeId, char *Name, Uint64 VfsInode)
 * \brief Create a new VFS Node
 */
vfs_node *Ext2_int_CreateNode(tExt2_Disk *Disk, Uint InodeId, char *Name, Uint64 VfsInode)
{
	tExt2_Inode	inode;
	vfs_node	retNode;
	vfs_node	*tmpNode;
	
	if( !Ext2_int_ReadInode(Disk, InodeId, &inode) )
		return NULL;
	
	if( (tmpNode = inode_getCache(Disk->CacheID, VfsInode)) )
		return tmpNode;
	
	// Set node name
	retNode.nameLength = strlen(Name);
	retNode.name = malloc( retNode.nameLength + 1 );
	strcpy(retNode.name, Name);
	
	//LogF(" Ext2_int_CreateNode: retNode.name = %i '%s'\n", retNode.nameLength, retNode.name);
	
	// Set identifiers
	retNode.inode = VfsInode;
	retNode.impl = InodeId;
	retNode.ImplPtr = Disk;
	
	// Set file length
	retNode.length = inode.i_size;
	
	// Set Access Permissions
	retNode.uid = inode.i_uid;
	retNode.gid = inode.i_gid;
	retNode.mode = inode.i_mode & 0777;
	
	//  Set Function Pointers
	retNode.read = Ext2_Read;
	retNode.write = Ext2_Write;
	retNode.close = Ext2_CloseFile;
	retNode.relink = NULL;	//Ext2_Relink;
	
	switch(inode.i_mode & EXT2_S_IFMT)
	{
	// Symbolic Link
	case EXT2_S_IFLNK:
		retNode.flags = VFS_FFLAG_SYMLINK;
		break;
	// Regular File
	case EXT2_S_IFREG:
		retNode.flags = 0;
		break;
	// Directory
	case EXT2_S_IFDIR:
		retNode.readdir = Ext2_ReadDir;
		retNode.finddir = Ext2_FindDir;
		retNode.mknod = Ext2_MkNod;
		retNode.flags = VFS_FFLAG_DIRECTORY;
		break;
	// Unknown, Write protect and hide it to be safe 
	default:
		retNode.flags = VFS_FFLAG_READONLY|VFS_FFLAG_HIDDEN;
		break;
	}
	
	// Check if the file should be hidden
	if(Name[0] == '.')	retNode.flags |= VFS_FFLAG_HIDDEN;
	
	// Set Timestamps
	retNode.atime = unow();
	retNode.mtime = inode.i_mtime * 1000;
	retNode.ctime = inode.i_ctime * 1000;
	
	// Save in node cache and return saved node
	return inode_cacheNode(Disk->CacheID, VfsInode, &retNode);
}

/**
 * \fn int Ext2_int_ReadInode(tExt2_Disk *Disk, Uint InodeId, tExt2_Inode *Inode)
 * \brief Read an inode into memory
 */
int Ext2_int_ReadInode(tExt2_Disk *Disk, Uint InodeId, tExt2_Inode *Inode)
{
	 int	group, subId;
	
	//LogF("Ext2_int_ReadInode: (Disk=%p, InodeId=%i, Inode=%p)\n", Disk, InodeId, Inode);
	
	if(InodeId == 0)
		return 0;
	
	InodeId --;	// Inodes are numbered starting at 1
	
	group = InodeId / Disk->SuperBlock.s_inodes_per_group;
	subId = InodeId % Disk->SuperBlock.s_inodes_per_group;
	
	//LogF(" Ext2_int_ReadInode: group=%i, subId = %i\n", group, subId);
	
	//Seek to Block - Absolute
	vfs_seek(Disk->fd, Disk->Groups[group].bg_inode_table * Disk->BlockSize, SEEK_SET);
	//Seeek to inode - Relative
	vfs_seek(Disk->fd, sizeof(tExt2_Inode)*subId, SEEK_CUR);
	vfs_read(Disk->fd, sizeof(tExt2_Inode), Inode);
	return 1;
}

/**
 * \fn Uint64 Ext2_int_GetBlockAddr(tExt2_Disk *Disk, Uint32 *Blocks, int BlockNum)
 * \brief Get the address of a block from an inode's list
 * \param Disk	Disk information structure
 * \param Blocks	Pointer to an inode's block list
 * \param BlockNum	Block index in list
 */
Uint64 Ext2_int_GetBlockAddr(tExt2_Disk *Disk, Uint32 *Blocks, int BlockNum)
{
	Uint32	*iBlocks;
	// Direct Blocks
	if(BlockNum < 12)
		return (Uint64)Blocks[BlockNum] * Disk->BlockSize;
	
	// Single Indirect Blocks
	iBlocks = malloc( Disk->BlockSize );
	VFS_ReadAt(Disk->fd, (Uint64)Blocks[12]*Disk->BlockSize, Disk->BlockSize, iBlocks);
	
	BlockNum -= 12;
	if(BlockNum < 256) {
		BlockNum = iBlocks[BlockNum];
		free(iBlocks);
		return (Uint64)BlockNum * Disk->BlockSize;
	}
	
	// Double Indirect Blocks
	if(BlockNum < 256*256)
	{
		VFS_ReadAt(Disk->fd, (Uint64)Blocks[13]*Disk->BlockSize, Disk->BlockSize, iBlocks);
		VFS_ReadAt(Disk->fd, (Uint64)iBlocks[BlockNum/256]*Disk->BlockSize, Disk->BlockSize, iBlocks);
		BlockNum = iBlocks[BlockNum%256];
		free(iBlocks);
		return (Uint64)BlockNum * Disk->BlockSize;
	}
	// Triple Indirect Blocks
	VFS_ReadAt(Disk->fd, (Uint64)Blocks[14]*Disk->BlockSize, Disk->BlockSize, iBlocks);
	VFS_ReadAt(Disk->fd, (Uint64)iBlocks[BlockNum/(256*256)]*Disk->BlockSize, Disk->BlockSize, iBlocks);
	VFS_ReadAt(Disk->fd, (Uint64)iBlocks[(BlockNum/256)%256]*Disk->BlockSize, Disk->BlockSize, iBlocks);
	BlockNum = iBlocks[BlockNum%256];
	free(iBlocks);
	return (Uint64)BlockNum * Disk->BlockSize;
}
